/* $Id: Tournament.java 39 2009-07-24 22:48:06Z h_m_r@gmx.de $ */
package org.jugger.tournament.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;

import org.jugger.tournament.model.ranking.Ranking;
import org.jugger.tournament.model.ranking.RankingComparator;
import org.jugger.tournament.model.score.ScoreCalculator;


/**
 * @author Sven Hamer
 */
public class Tournament {

	// Base data
	private Vector<Team> teams = new Vector<Team>();
	private TournamentMeta meta = new TournamentMeta();

	// Cached/helper/lookup data

	/** TeamStats for each Round */
	private Vector<ArrayList<TeamStats>> rankings = new Vector<ArrayList<TeamStats>>();
	private final RankingComparator rankingComparator;
	private final ScoreCalculator scoreCalculator;

	/** Rounds of this tournament */
	private Vector<Round> rounds = new Vector<Round>();

	/** Lookup table for team matches */
	private HashMap<Team, HashSet<Team>> hasPlayedAgainst = new HashMap<Team, HashSet<Team>>();

	public Tournament(RankingComparator rankingComparator, ScoreCalculator scoreCalculator, Set<Team> teams, Collection<MatchTemplates> rounds) {
		this(rankingComparator,scoreCalculator,teams);

		// set rounds
		int roundCount = rounds.size();
		int roundNumber = 1;
		this.rounds.clear();
		for(MatchTemplates matchTemplates: rounds) {
			HashSet<Team> foundTeamsInRound = new HashSet<Team>();

			if(matchTemplates.size()*2 != teams.size())
				throw new RuntimeException("Invalid match quantity. Each team has to play each round exactly one time.");

			Round round = new Round();
			for(MatchTemplate matchTemplate: matchTemplates) {

				// create match from template at this point.
				// only matches up to current round must be created.
				// => they affect current ranking creation, which will be snapshoted each iteration
				Match match = matchTemplate.createMatch(scoreCalculator);
				round.add(match);

				Team teamA = match.getTeamA();
				Team teamB = match.getTeamB();
				if(!teams.contains(teamA) || !teams.contains(teamB))
					throw new RuntimeException("At least one match contains a team, that is not in the tournament.");

				foundTeamsInRound.add(teamA);
				foundTeamsInRound.add(teamB);

				// every round beside last round has to be _finished_
				if(roundNumber != roundCount) {
					if(!match.isFinished())
						throw new RuntimeException("Every round beside last round has to be finished.");
				}
			}
			if(teams.size() != foundTeamsInRound.size())
				throw new RuntimeException("At least one team had no match in one given round.");

			this.rounds.add(round);
			if(roundNumber != roundCount) {
				// calculate ranking, etc
				updateData();
			}

			++roundNumber;
		}
	}

	public Tournament(RankingComparator rankingComparator, ScoreCalculator scoreCalculator, Set<Team> teams) {

		if (teams.size() % 2 == 1)
			throw new IllegalArgumentException("Number of teams must be even.");

		this.rankingComparator = rankingComparator;
		this.scoreCalculator = scoreCalculator;
		this.teams.addAll(teams);

		// empty lookup table
		for (Team team : teams) {
			hasPlayedAgainst.put(team, new HashSet<Team>());
		}

		// calculate first round
		calculateNextRound();
	}

	public TournamentMeta getMeta() {
		return meta;
	}

	public void setMeta(TournamentMeta meta) {
		this.meta = meta;
	}

	private void updateData() {

		for (Match match : rounds.lastElement()) {

			// update hasPlayedAgainst lookup table...
			hasPlayedAgainst.get(match.getTeamA()).add(match.getTeamB());
			hasPlayedAgainst.get(match.getTeamB()).add(match.getTeamA());
		}

		// save current ranking
		ArrayList<TeamStats> ranking = new ArrayList<TeamStats>();
		for(TeamStats teamStats: getCurrentRanking()) {
			ranking.add(new TeamStats(teamStats));
		}
		rankings.add(ranking);

	}

	/**
	 * Calculates the matches for the next round. This could be done, only if
	 * all matches of the current round are finished.
	 */
	public void calculateNextRound() {

		if(!isReadyForNextRound())
			throw new RuntimeException("Not all matches are finished.");

		Round nextRound = null;
		if(rounds.isEmpty()) {
			// first round => select random team pairs and create matches
			nextRound = RoundCalculator.calculateRandomRound(scoreCalculator, teams, hasPlayedAgainst);
		}
		else {
			// not first round => select following team pairs
			updateData();
			nextRound = RoundCalculator.calculateRound(scoreCalculator, teams, hasPlayedAgainst, getCurrentRanking());
		}

		rounds.add(nextRound);
	}

	/**
	 * Recalculates current round. All matches, that are recalculatable, will be recalculated.
	 */
	public void recalculateCurrentRound() {
		// recalculate current round.
		// -> first round is calculated randomly, second+ round use last round's ranking
		if(rounds.size() == 0)
			throw new RuntimeException("No round exists yet.");

		// round, that has to be recalculated
		Round currentRound = rounds.lastElement();

		// get teams, that have not played yet
		Vector<Team> teamsLeft = new Vector<Team>();
		Vector<Integer> replaceMatches = new Vector<Integer>();
		int matchIndex = 0;
		for(Match match: currentRound) {
			if(match.isRecalculatable()) {
				teamsLeft.add(match.getTeamA());
				teamsLeft.add(match.getTeamB());
				replaceMatches.add(matchIndex);
			}
			++matchIndex;
		}

		// recalculate matches
		Round recalculatedRound = RoundCalculator.calculateRound(scoreCalculator, teamsLeft, hasPlayedAgainst, rounds.size() == 1 ? null : rankings.lastElement());
		// replace matches
		for(int i = 0; i<replaceMatches.size(); ++i) {
			currentRound.set( replaceMatches.get(i), recalculatedRound.get(i) );
		}
	}

	public Collection<Team> getTeamsNotPlayedInCurrentRound() {
		Vector<Team> teamsLeft = new Vector<Team>();
		for(Match match: getCurrentRound()) {
			if(match.isRecalculatable()) {
				teamsLeft.add(match.getTeamA());
				teamsLeft.add(match.getTeamB());
			}
		}
		return teamsLeft;
	}


	public boolean isReadyForNextRound() {
		if(rounds.size() == 0)
			return true;
		return rounds.lastElement().isReadyForNextRound();
	}

	/**
	 * Calculates current ranking based on current teamstats.
	 * Each finished match changes the team ranking instantly.
	 * @return
	 */
	public Collection<TeamStats> getCurrentRanking() {
		Ranking result = new Ranking(rankingComparator);
		for(Team team: teams) {
			result.add( team.getCurrentTeamStats() );
		}
		return result;
	}

	public Collection<TeamStats> getLastRanking() {
		return getRankingAfterRound(rankings.size());
	}


	/**
	 * Returns the ranking after the given round number.
	 * Round number starts with 1.
	 * @param roundNumber
	 * @return
	 */
	public Collection<TeamStats> getRankingAfterRound(int roundNumber) {
		if(roundNumber < 1 || roundNumber > rankings.size() )
			throw new IllegalArgumentException("Invalid round number.");
		return rankings.get(roundNumber-1);
	}

	public int countFinishedRounds() {
		return rankings.size();
	}

	public Collection<Round> getRounds() {
		return rounds;
	}

	public Round getRound(int roundNumber) {
		if(roundNumber < 1 || roundNumber > rounds.size() )
			throw new IllegalArgumentException("Invalid round number.");
		return rounds.get(roundNumber-1);
	}

	public Round getCurrentRound() {
		return rounds.lastElement();
	}

	public RankingComparator getRankingComparator() {
		return rankingComparator;
	}

	public ScoreCalculator getScoreCalculator() {
		return scoreCalculator;
	}

	public Collection<Team> getTeams() {
		return teams;
	}

	public void switchTeamsInCurrentRound(Team team1, Team team2) {
		Round currentRound = rounds.lastElement();

		int index1 = -1;
		int index2 = -1;
		Match match1 = null;
		Match match2 = null;
		boolean isTeamA1 = false;
		boolean isTeamA2 = false;

		for(int i = 0; i<currentRound.size(); ++i) {
			Match match = currentRound.get(i);
			if(match.containsTeam(team1)) {
				index1 = i;
				match1 = match;
				isTeamA1 = match.getTeamA() == team1;
			}
			if(match.containsTeam(team2)) {
				index2 = i;
				match2 = match;
				isTeamA2 = match.getTeamA() == team2;
			}
		}

		if(match1 == null || match2 == null)
			throw new RuntimeException("Team not in tournament.");

		if(!match1.isRecalculatable() || !match2.isRecalculatable())
			throw new RuntimeException("Match is not recalculatable.");

		if(index1 == index2) {
			// same match => switch team A/B, if both given teams are distinct...
			if(team1 != team2) {
				Match newMatch = new Match(scoreCalculator, match1.getTeamB(), match1.getTeamA());
				currentRound.set(index1, newMatch);
			}
		}
		else {
			// different matches
			Match newMatch;
			Team newTeamA, newTeamB;

			// switch team1 against team2 on first match
			newTeamA = isTeamA1 ? team2 : match1.getTeamA();
			newTeamB = isTeamA1 ? match1.getTeamB() : team2;

			newMatch = new Match(scoreCalculator, newTeamA, newTeamB);
			currentRound.set(index1, newMatch);

			newTeamA = isTeamA2 ? team1 : match2.getTeamA();
			newTeamB = isTeamA2 ? match2.getTeamB() : team1;
			newMatch = new Match(scoreCalculator, newTeamA, newTeamB);
			currentRound.set(index2, newMatch);
		}
	}

	public boolean teamsHavePlayedAgainst(Team a, Team b) {
		return hasPlayedAgainst.get(a).contains(b);
	}

	public boolean teamsHavePlayedAgainst(Match match) {
		return teamsHavePlayedAgainst(match.getTeamA(), match.getTeamB());
	}

	public boolean hasTeam(Team team, Team excludeFromTest) {
		for(Team t: teams) {
			if(t != excludeFromTest && t.equals(team))
				return true;
		}
		return false;
	}



	// add additional teams
	public boolean addTeams(Set<Team> newTeams) {
		// only in first round!
		if(countFinishedRounds() == 0) {
			// filter already existing teams
			HashSet<Team> newTeamsFiltered = new HashSet<Team>();
			for(Team t: newTeams) {
				if(!teams.contains(t)) {
					newTeamsFiltered.add(t);
				}
			}
			// only pairs of teams
			if(newTeamsFiltered.size() % 2 == 0) {

				teams.addAll(newTeamsFiltered);
				// add new matches
				Team t1 = null;
				for(Team t: newTeamsFiltered) {
					if(t1 == null) {
						t1 = t;
					}
					else {
						Match m = new Match(scoreCalculator, t1, t);
						getCurrentRound().add(m);
						t1 = null;
					}
				}

				return true;
			}
		}

		return false;
	}
}
